/*
Copyright 2007 Infordata S.p.A.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/*
!!V 07/04/97 rel. 1.00 - start of revisions history.
    14/05/97 rel. 1.00a- ...
    19/05/97 rel. 1.01 - added support for telnet proxy.
    15/07/07 rel. 1.02c- removed throws UnknownHostException form connect method.
             finalize() method added.
    24/09/97 rel. 1.05 - DNCX project.
    03/03/98 rel. _.___- SWING and reorganization.
    ***
    30/06/98 rel. _.___- Swing, JBuilder2 e VSS.
 */


package net.infordata.em.tnprot;


import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;



///////////////////////////////////////////////////////////////////////////////

/**
 * Handles a telnet protocol connection.
 * A telnet emulator must implement XITelnetEmulator interface to receive notification
 * about the telnet connection.
 * Supports RFC885 End of record. 
 *
 * @see    XITelnetEmulator
 *
 * @author   Valentino Proietti - Infordata S.p.A.
 */
public class XITelnet {

  private static final Logger LOGGER = Logger.getLogger(XITelnet.class.getName());
  
  /**
   * Telnet options or flags.
   */
  public static final byte TELOPT_BINARY = 0;
  public static final byte TELOPT_ECHO = 1;
  public static final byte TELOPT_RCP = 2;
  public static final byte TELOPT_SGA = 3;
  public static final byte TELOPT_NAMS = 4;
  public static final byte TELOPT_STATUS = 5;
  public static final byte TELOPT_TM = 6;
  public static final byte TELOPT_RCTE = 7;
  public static final byte TELOPT_NAOL = 8;
  public static final byte TELOPT_NAOP = 9;
  public static final byte TELOPT_NAOCRD = 10;
  public static final byte TELOPT_NAOHTS = 11;
  public static final byte TELOPT_NAOHTD = 12;
  public static final byte TELOPT_NAOFFD = 13;
  public static final byte TELOPT_NAOVTS = 14;
  public static final byte TELOPT_NAOVTD = 15;
  public static final byte TELOPT_NAOLFD = 16;
  public static final byte TELOPT_XASCII = 17;
  public static final byte TELOPT_LOGOUT = 18;
  public static final byte TELOPT_BM = 19;
  public static final byte TELOPT_DET = 20;
  public static final byte TELOPT_SUPDUP = 21;
  public static final byte TELOPT_SUPDUPOUTPUT = 22;
  public static final byte TELOPT_SNDLOC = 23;
  public static final byte TELOPT_TTYPE = 24;
  public static final byte TELOPT_EOR = 25;
  public static final byte TELOPT_TUID = 26;
  public static final byte TELOPT_OUTMRK = 27;
  public static final byte TELOPT_TTYLOC = 28;
  public static final byte TELOPT_3270REGIME = 29;
  public static final byte TELOPT_X3PAD = 30;
  public static final byte TELOPT_NAWS = 31;
  public static final byte TELOPT_TSPEED = 32;
  public static final byte TELOPT_LFLOW = 33;
  public static final byte TELOPT_LINEMODE = 34;
  public static final byte TELOPT_XDISPLOC = 35;
  public static final byte TELOPT_OLD_ENVIRON = 36;
  public static final byte TELOPT_AUTHENTICATION = 37;
  public static final byte TELOPT_ENCRYPT = 38;
  public static final byte TELOPT_NEW_ENVIRON = 39;
  public static final String[] TELOPT = 
                                  {"BINARY", "ECHO", "RCP", "SUPPRESS GO AHEAD", "NAME", 
                                   "STATUS", "TIMING MARK", "RCTE", "NAOL", "NAOP", "NAOCRD", 
                                   "NAOHTS", "NAOHTD", "NAOFFD", "NAOVTS", "NAOVTD", "NAOLFD", 
                                   "EXTEND ASCII", "LOGOUT", "BYTE MACRO",
                                   "DATA ENTRY TERMINAL", "SUPDUP", "SUPDUP OUTPUT", 
                                   "SEND LOCATION", "TERMINAL TYPE", "END OF RECORD", 
                                   "TACACS UID", "OUTPUT MARKING", "TTYLOC", "3270 REGIME", 
                                   "X.3 PAD", "NAWS", "TSPEED", "LFLOW", "LINEMODE",
                                   "XDISPLOC", "OLD-ENVIRON", "AUTHENTICATION", "ENCRYPT", 
                                   "NEW-ENVIRON", "<UNKNOWN>" };

  /**
   * Telnet escapes
   */ 
  static final byte IAC  = (byte)0xFF;   
  static final byte DONT = (byte)0xFE;
  static final byte DO   = (byte)0xFD;
  static final byte WONT = (byte)0xFC;
  static final byte WILL = (byte)0xFB;
  static final byte SB   = (byte)0xFA;
  static final byte SE   = (byte)0xF0;
  static final byte EOR  = (byte)0xEF;
  static final String[] TELCMD = {
    "IAC", "DONT", "DO", "WONT", 
    "WILL", "SB", "GA", "EL",
    "EC", "AYT", "AO", "IP",
    "BRK", "DATA MARK", "NOP", "SE",
    "EOR"}; 
      
  static final byte SEND = (byte)0x01;
  static final byte IS   = (byte)0x00;

  // parser states for IAC sequences
  static final int  SIAC_START = 0;    
  static final int  SIAC_WCMD  = 1;
  static final int  SIAC_WOPT  = 2;
  static final int  SIAC_WSTR  = 3;
  
  private String         ivHost;                    
  private int            ivPort;                       
  
  /**
   * if null then the connection is closed
   */
  transient private Socket         ivSocket;                       
  transient private InputStream    ivIn;                 
  transient private OutputStream   ivOut;               
  transient private RxThread       ivReadTh;
  
  transient private byte           ivIACCmd;
  transient private byte           ivIACOpt;
  transient private String         ivIACStr;

  transient private boolean[]      ivLocalFlags = new boolean[128];
  transient private boolean[]      ivRemoteFlags = new boolean[128];
  
  private boolean[]      ivLocalReqFlags = new boolean[128];
  private boolean[]      ivRemoteReqFlags = new boolean[128];
  
  private String         ivTermType;
  private String         ivEnvironment;

  transient private int              ivIACParserStatus = SIAC_START;

  private   XITelnetEmulator ivEmulator;

  transient private String  ivFirstHost;      //!!1.01
  transient private String  ivSecondHost;     //!!1.01

  transient private boolean ivUsed = false;   //!!1.07

  /**
   * Converts byte to int without sign
   */
  public static final int toInt(byte bb) {
    return ((int)bb & 0xff);
  }


  /**
   * Converts byte to hexadecimal string rappresentation
   */
  public static final String toHex(byte bb) {
    String hex = Integer.toString(toInt(bb), 16);
    return "00".substring(hex.length()) + hex;
  }

  public static final String toHex(byte[] buf, int len) {
    StringBuilder sb = new StringBuilder(len * 4);
    for (int i = 0; i < len; i++) {
      sb.append(toHex(buf[i])).append(' ');
    }
    return sb.toString();
  }

  public static final String toHex(byte[] buf) {
    return toHex(buf, buf.length);
  }


  /**
   * Uses telnet default port for socket connection.
   */
  public XITelnet(String aHost) {
    this(aHost, 23);
  }


  /**
   * Uses the given port for socket connection.
   */
  public XITelnet(String aHost, int aPort) {
    if (aHost == null)
      throw new IllegalArgumentException("Host cannot be null");

    ivHost = aHost;
    ivPort = aPort;

    try {
      StringTokenizer st  = new StringTokenizer(ivHost, "#");
      ivFirstHost = st.nextToken();
      ivSecondHost = st.nextToken();
    }
    catch (NoSuchElementException ex) {
    }
  }


  /**
   * Returns the host-name or ip address.
   */
  public String getHost() {
    return ivHost;
  }


  /**
   * Returns the telnet port.
   */
  public int getPort() {
    return ivPort;
  }


  /**
   * Sets the receiving notifications XITelnetEmulator instance.
   */
  public void setEmulator(XITelnetEmulator aEmulator) {
    ivEmulator = aEmulator;
  }


  /**
   */
  public boolean isConnected() {
    return (ivSocket != null);
  }


  /**
   * Sets the telnet terminal type option.
   * Must be used before that a connection is established.
   */
  public void setTerminalType(String aTerminalType) {
    if (isConnected())
      throw new IllegalArgumentException("Telnet already connected");

    setLocalReqFlag(TELOPT_TTYPE, true);
    ivTermType = aTerminalType;
  }


  /**
   */
  public String getTerminalType() {
    return ivTermType;
  }


  /**
   * Sets the telnet environment option.
   * Must be used before that a connection is established.
   */
  public void setEnvironment(String aEnv) {
    if (isConnected())
      throw new IllegalArgumentException("Telnet already connected");

    setLocalReqFlag(TELOPT_NEW_ENVIRON, true);
    ivEnvironment = aEnv;
  }


  /**
   */
  public String getEnvironment() {
    return ivEnvironment;
  }


  /**
   * Sets the local requested flags.
   * Must be used before that a connection is established.
   *
   * @param    flag  use a TELOPT_ constant.
   * @param    b     to be requested ?
   */
  public void setLocalReqFlag(byte flag, boolean b) {
    if (isConnected())
      throw new IllegalArgumentException("Telnet already connected");

    ivLocalReqFlags[flag] = b;
  }


  /**
   * Sets the remote requested flags.
   * Must be used before that a connection is established.
   *
   * @param    flag  use a TELOPT_ constant.
   * @param    b     to be requested ?
   */
  public void setRemoteReqFlag(byte flag, boolean b) {
    if (isConnected())
      throw new IllegalArgumentException("Telnet already connected");

    ivRemoteReqFlags[flag] = b;
  }


  /**
   * Can be used to query a local flag status.
   */
  public boolean isLocalFlagON(byte flag) {
    return ivLocalFlags[flag];
  }


  /**
   * Can be used to query a remote flag status.
   */
  public boolean isRemoteFlagON(byte flag) {
    return ivRemoteFlags[flag];
  }


  /**
   * Tryes to establish a telnet connection.
   * If a connection is already established then a call to disconnect() is maded.
   */
  public synchronized void connect() {
    if (ivUsed)
      throw new IllegalArgumentException("XITelnet cannot be recycled");
      
    disconnect();

    connecting();

    try {
      ivSocket = new Socket(ivFirstHost, ivPort);

      ivIn = ivSocket.getInputStream();
      ivOut = ivSocket.getOutputStream();

      ivReadTh = new RxThread();
      ivReadTh.start();

      ivUsed = true;

      // avvio scambio caratteristiche con server
      /* NO!!! da problemi con AS/400
      for (int i = 0; i < ivRemoteReqFlags.length; i++) {
        if (ivRemoteReqFlags[i])
          sendIACCmd(DO, (byte)i);
      }
      for (int i = 0; i < ivLocalReqFlags.length; i++) {
        if (ivLocalReqFlags[i])
          sendIACCmd(WILL, (byte)i);
      }
      */

      connected();
    }
    catch (IOException ex) {
      catchedIOException(ex);
    }
  }


  /**
   */
  private void closeSocket() {
    if (ivSocket != null) {
      try {
        if (LOGGER.isLoggable(Level.FINE))
          LOGGER.fine("closing...");
        ivSocket.close();
      }
      catch (IOException ex) {
        // non richiamare catchedIOException();
      }
      ivSocket = null;
      ivIn = null;
      ivOut = null;

      disconnected();
    }
  }


  /**
   * Closes the telnet connection.
   */
  public synchronized void disconnect() { //!!V 03/03/98
    if (ivReadTh != null) {
      ivReadTh.terminate();
      ivReadTh = null;
    }
    closeSocket();
  }


  /**
   * Telnet IAC parser.
   */
  protected int processIAC(byte bb) throws IOException {
    int res = 1;

    switch (ivIACParserStatus) {
      //
      case SIAC_START:
        switch (bb) {
          case IAC:
            if (LOGGER.isLoggable(Level.FINE))
              LOGGER.fine("IAC");

            ivIACParserStatus = SIAC_WCMD;
            res = 0;
            break;
        }
        break;
      // CMD
      case SIAC_WCMD:
        if (LOGGER.isLoggable(Level.FINE)) {
          StringBuilder sb = new StringBuilder();
          sb.append(" r " + bb + " ");
          try {
            sb.append(TELCMD[-(bb + 1)] + " ");
          }
          catch (Exception ex) {
          }
          LOGGER.fine(sb.toString());
        }

        switch (bb) {
          //
          case IAC:
            ivIACParserStatus = SIAC_START;
            break;
          //
          case EOR:
            ivIACParserStatus = SIAC_START;
            res = 0;
            if (ivLocalFlags[TELOPT_EOR])
              receivedEOR();
            break;
          //
          case WILL:
          case WONT:
          case DO:
          case DONT:
            ivIACCmd = bb;
            ivIACParserStatus = SIAC_WOPT;
            res = 0;
            break;
          //
          case SB:
            ivIACStr = "";
            ivIACCmd = bb;
            ivIACParserStatus = SIAC_WOPT;
            res = 0;
            break;
          //
          case SE:
            ivIACParserStatus = SIAC_START;

            if (LOGGER.isLoggable(Level.FINE))
              LOGGER.fine("SE " + TELOPT[ivIACOpt]);

            res = 0;
            if (ivLocalFlags[ivIACOpt]) {
              switch (ivIACOpt) {
                case TELOPT_TTYPE:
                  sendIACStr(SB, TELOPT_TTYPE, true, ivTermType);
                  break;
                case TELOPT_NEW_ENVIRON:
                  sendIACStr(SB, TELOPT_NEW_ENVIRON, true, ivEnvironment);
                  break;
                default:
                  unhandledRequest(ivIACOpt, ivIACStr);
                  break;
              }
            }
            break;
          //
          default:
            ivIACParserStatus = SIAC_START;
            res = 0;
            break;
        }
        break;
      // OPT
      case SIAC_WOPT:
        ivIACOpt = bb;

        if (LOGGER.isLoggable(Level.FINE))
          LOGGER.fine(TELOPT[ivIACOpt]);

        res = 0;
        switch (ivIACCmd) {
          case SB:
            break;
          case DONT:
            if (ivLocalFlags[ivIACOpt]) {
              ivLocalFlags[ivIACOpt] = false;
              sendIACCmd(WONT, ivIACOpt);

              localFlagsChanged(ivIACOpt);
            }
            break;
          case DO:
            // opzione locale accettabile
            if (ivLocalReqFlags[ivIACOpt]) {
              if (!ivLocalFlags[ivIACOpt]) {
                ivLocalFlags[ivIACOpt] = true;
                sendIACCmd(WILL, ivIACOpt);

                localFlagsChanged(ivIACOpt);
              }
            }
            else
              sendIACCmd(WONT, ivIACOpt);
            break;
          case WONT:
            if (ivRemoteFlags[ivIACOpt]) {
              ivRemoteFlags[ivIACOpt] = false;
              sendIACCmd(DONT, ivIACOpt);

              remoteFlagsChanged(ivIACOpt);
            }
            break;
          case WILL:
            // opzione remota accettabile
            if (ivRemoteReqFlags[ivIACOpt]) {
              if (!ivRemoteFlags[ivIACOpt]) {
                ivRemoteFlags[ivIACOpt] = true;
                sendIACCmd(DO, ivIACOpt);

                remoteFlagsChanged(ivIACOpt);
              }
            }
            else
              sendIACCmd(DONT, ivIACOpt);
            break;
        }

        if (ivIACCmd != SB)
          ivIACParserStatus = SIAC_START;
        else
          ivIACParserStatus = SIAC_WSTR;
        break;
      //
      case SIAC_WSTR:
        res = 0;
        switch (bb) {
          case IAC:
            ivIACParserStatus = SIAC_WCMD;
            break;
          default:
            ivIACStr += (char)bb;
            break;
        }
        break;
    }

    return res;
  }


  /**
   * Sends an telnet EOR sequence.
   */
  public void sendEOR() throws IOException {
    byte[] buf = {IAC, EOR};

    try {
      ivOut.write(buf);
      ivOut.flush();
    }
    catch (IOException ex) {
      catchedIOException(ex);
    }
  }


  /**
   * Sends a telnet IAC sequence.
   */
  public void sendIACCmd(byte aCmd, byte aOpt) {
    if (LOGGER.isLoggable(Level.FINE))
      LOGGER.fine(" t " + aCmd + " " + TELCMD[-(aCmd + 1)] + " " +
          TELOPT[aOpt]);

    byte[] buf = {IAC, aCmd, aOpt};

    try {
      ivOut.write(buf);
      ivOut.flush();
    }
    catch (IOException ex) {
      catchedIOException(ex);
    }
  }


  /**
   * Sends a telnet IAC sequence with a string argument.
   */
  public void sendIACStr(byte aCmd, byte aOpt, boolean sendIS, String aString) {
    if (LOGGER.isLoggable(Level.FINE))
      LOGGER.fine("t " + aCmd + " " + TELCMD[-(aCmd + 1)] + " " +
          TELOPT[aOpt] + " " + aString);

    byte[] endBuf = {IAC, SE};
    byte[] startBuf = 
      sendIS ? new byte[] {IAC, aCmd, aOpt, IS} : new byte[] {IAC, aCmd, aOpt};
//    String str = new String(startBuf) + aString + new String(endBuf);
//    byte[] buf = str.getBytes();

    try {
      ivOut.write(startBuf);
      ivOut.write(aString.getBytes());
      ivOut.write(endBuf);
      ivOut.flush();
    }
    catch (IOException ex) {
      catchedIOException(ex);
    }
  }


  /**
   * Sends a data buffer (IAC bytes are doubled).
   */
  public void send(byte[] aBuf, int aLen) {
    try {
      for (int i = 0; i < aLen; i++) {
        ivOut.write(aBuf[i]);
        if (aBuf[i] == IAC)
          ivOut.write(IAC);
      }
    }
    catch (IOException ex) {
      catchedIOException(ex);
    }
  }


  /**
   * Sends a data buffer (IAC bytes are doubled).
   */
  public void send(byte[] aBuf) {
    send(aBuf, aBuf.length);
  }


  /**
   * Flushes output buffer.
   */
  public void flush() {
    try {
      ivOut.flush();
    }
    catch (IOException ex) {
      catchedIOException(ex);
    }
  }


  /**
   * Called just before trying to connect.
   */
  protected void connecting() {
    if (ivEmulator != null)
      ivEmulator.connecting();
  }


  /**
   * Called after that a connection is established.
   */
  protected void connected() {
    if (ivSecondHost != null && !ivSecondHost.equals(""))
      send((new String(ivSecondHost + "\n")).getBytes());

    if (ivEmulator != null)
      ivEmulator.connected();
  }


  /**
   * Called after that the connection is closed.
   */
  protected void disconnected() {
    if (ivEmulator != null)
      ivEmulator.disconnected();
  }


  /**
   * Called when an IOException is catched.
   */
  protected synchronized void catchedIOException(IOException ex) {
    if (LOGGER.isLoggable(Level.FINE))
      LOGGER.log(Level.FINE, "" , ex);

    try {
      if (ivEmulator != null)
        ivEmulator.catchedIOException(ex);
    }
    finally {
      disconnect();
    }
  }


  /**
   * Called when an unhandled IAC request is received.
   */
  protected void unhandledRequest(byte aIACOpt, String aIACStr) {
    if (ivEmulator != null)
      ivEmulator.unhandledRequest(aIACOpt, aIACStr);
  }


  /**
   * Called when a local flags has been changed.
   */
  protected void localFlagsChanged(byte aIACOpt) {
    if (ivEmulator != null)
      ivEmulator.localFlagsChanged(aIACOpt);
  }


  /**
   * Called when a remote flags has been changed.
   */
  protected void remoteFlagsChanged(byte aIACOpt) {
    if (ivEmulator != null)
      ivEmulator.remoteFlagsChanged(aIACOpt);
  }


  /**
   * Called when data are received.
   * Data are already cleared from IAC sequence.
   * NOTE: receivedData is always called in the receiving thread.
   */
  protected void receivedData(byte[] buf, int len) {
    if (LOGGER.isLoggable(Level.FINEST)) {
      LOGGER.finest(toHex(buf, len));
    }

    if (ivEmulator != null)
      ivEmulator.receivedData(buf, len);
  }


  /**
   * Called when a telnet EOR sequence is received.
   * NOTE: receivedEOR is always called in the receiving thread.
   */
  protected void receivedEOR() {
    if (LOGGER.isLoggable(Level.FINE)) 
      LOGGER.fine("EOR");

    if (ivEmulator != null)
      ivEmulator.receivedEOR();
  }


  /**
   */
  @Override
  protected void finalize() throws Throwable {
    disconnect();
    super.finalize();
  }


  /**
   * Only for test purposes.
   */
//  static void showFlagsStatus(boolean[] aFlags) {
//    for (int i = TELOPT_BINARY; i <= TELOPT_NEW_ENVIRON; i++) {
//      Diagnostic.getOut().print(TELOPT[i] + " " + aFlags[i] + "  -  ");
//      if ((i % 3) == 0)
//        Diagnostic.getOut().println();
//    }
//    Diagnostic.getOut().println();
//  }


  /**
   * Used only for test purposes.
   */
  public static void main(String[] argv) {
    XITelnet tn = new XITelnet("192.168.0.1#192.168.0.4");
    tn.setTerminalType("IBM-3477-FC");

    tn.setLocalReqFlag(TELOPT_BINARY, true);
    tn.setLocalReqFlag(TELOPT_TTYPE, true);
    //tn.setLocalReqFlag(TELOPT_NEW_ENVIRON, true);
    tn.setLocalReqFlag(TELOPT_EOR, true);

    tn.setRemoteReqFlag(TELOPT_BINARY, true);
    //tn.setRemoteReqFlag(TELOPT_ECHO, true);
    //tn.setRemoteReqFlag(TELOPT_SGA, true);
    //tn.setRemoteReqFlag(TELOPT_STATUS, true);
    tn.setRemoteReqFlag(TELOPT_EOR, true);

    try {
      tn.connect();

      Thread.sleep(10000);

      tn.disconnect();
    }
    catch (Exception ex) {
      ex.printStackTrace();
    }
  }


  //////////////////////////////////////////////////////////////////////////////

  /**
   */
  class RxThread extends Thread {

    private boolean ivTerminate = false;


    /**
     */
    public RxThread() {
      super("XITelnet rx thread");
    }


    /**
     */
    public void terminate() {
      ivTerminate = true;
      if (this != Thread.currentThread()) {  //!!V 03/03/98
        interrupt();
      }
    }


    /**
     * The receiving thread.
     */
    @Override
    public void run() {
      byte[] buf  = new byte[1024];
      byte[] rBuf = new byte[1024];
      int    len = 0;
      int    i, j;

      try {
        while(!ivTerminate) {
          try {
            len = ivIn.read(buf);
          }
          catch (InterruptedIOException iex) { //!!V 03/03/98
            len = 0;
          }

          // process all IAC commands
          for (i = 0, j = 0; i < len; i++) {
            rBuf[j] = buf[i];
            if ((ivIACParserStatus != SIAC_START) || (buf[i] == IAC)) {
              // if a IAC is received then split rx buffer
              if ((ivIACParserStatus == SIAC_START) && (buf[i] == IAC)) {
                if (j > 0)
                  receivedData(rBuf, j);
                j = 0;
              }
              j += processIAC(buf[i]);
            }
            else
              ++j;
          }

          if (j > 0)
            receivedData(rBuf, j);
        }
      }
      catch (IOException ex) {
        if (!ivTerminate)
          catchedIOException(ex);
      }
      /*
      catch (ThreadDeath ex) {
        throw ex;
      }
      */
    }
  }
}
